4.8 分配块或函数给字段
- 使用代码块或者调用一个函数初始化类里的字段
4.8.1 解决方案
设置字段等于需要的代码块或者函数
class Foo { // set 'text' equal to the result of the block of code val text = { var lines = "" try { lines = io.Source.fromFile("/etc/passwd").getLines.mkString } catch { case e: Exception => lines = "Error happened" } lines } println(text) } object Test extends App { val f = new Foo }
- 上面分配代码块给text字段和println语句都在类Foo的主体部分中,他们都是类的构造函数,当创建一个类的实例时他们会被执行。
同样方式。分配方法或者函数给类字段
class Foo { import scala.xml.XML // assign the xml field to the result of the load method val xml = XML.load("http://example.com/foo.xml") // more code here ... }
4.8.2 讨论
如果定义一个字段为lazy,意味着不会立马执行直到字段被获取调用:
class Foo { val text = io.Source.fromFile("/etc/passwd").getLines.foreach(println) } object Test extends App { val f = new Foo }
上面代码忽略潜在错误,如果运行在Unix系统,会打印/etc/passwd文件
class Foo { lazy val text = io.Source.fromFile("/etc/passwd").getLines.foreach(println) } object Test extends App { val f = new Foo }
- 上面声明成lazy字段的代码编译执行后,没有任何输出。因为text字段不会初始化直到它被获取。
4.9 设置未初始化的var字段类型
问题: 想要设置一个未初始化var字段类型,所以开始写代码如下,然后如何完成表达式。
var x =
4.9.1 解决方案
一般来说,定义字段为Option。对于具体的类型,比如String和numeric字段,可以指定默认的初始化值,下面的address字段可以定义成Option,初始化如下。
case class Person(var username: String, var password: String) { var age = 0 var firstName = "" var lastName = "" var address = None: Option[Address] } case class Address(city: String, state: String, zip: String)
使用Some[Address]赋值
val p = Person("alvinalexander", "secret") p.address = Some(Address("Talkeetna", "AK", "99676"))
如想要获取address字段,有很多方法可见20.6章。可以使用foreach循环打印:
p.address.foreach { a => println(a.city) println(a.state) println(a.zip) }
- 如果address未赋值,那么address是一个None,调用foreach没有问题,循环会自动跳出。如果已经赋值,那么address是一个Some[Address],循环会进入然后打印。
4.9.2 讨论
很容易创建一个Int和Double字段
var i = 0 // Int var d = 0.0 // Double
上面例子中编译器会自动区分需要的类型。如果需要不同的数字类型,方法如下:
var b: Byte = 0 var c: Char = 0 var f: Float = 0 var l: Long = 0 var s: Short = 0
- 查看更多
- Option class
- 不要设置字段为null,更多见20.5章:“Eliminate null Values from Your Code”
- 20.6章:“Using the Option/Some/None Pattern”
4.10 当继承类时处理构造函数参数
- 问题:当继承一个基类时,需要处理基类声明的构造函数参数以及子类新的参数
4.10.1 解决方案
往常一样使用val或者var构造函数参数声明基类,当定义子类构造函数时,去掉两个类中相同字段前的val或者var声明,当定义子类新的构造函数参数时使用val或者var声明。
class Person (var name: String, var address: Address) { override def toString = if (address == null) name else s"$name @ $address" } case class Address (city: String, state: String)
子类Employee
class Employee (name: String, address: Address, var age: Int) extends Person (name, address) { // rest of the class }
创建Employee实例
val teresa = new Employee("Teresa", Address("Louisville", "KY"), 25)
输出
scala> teresa.name res0: String = Teresa scala> teresa.address res1: Address = Address(Louisville,KY) scala> teresa.age res2: Int = 25
4.10.2 讨论
理解Scala编译器如何转换你的代码有助于理解子类构造函数参数如何工作,下面代码放到文件Person.scala中:
case class Address (city: String, state: String) class Person (var name: String, var address: Address) { override def toString = if (address == null) name else s"$name @ $address" }
上面字段是var变量,Scala编译器生成了获取器和修改器,编译Person.scala反编译Person.class如下:
$ javap Person Compiled from "Person.scala" public class Person extends java.lang.Object implements scala.ScalaObject{ public java.lang.String name(); public void name_$eq(java.lang.String); public Address address(); public void address_$eq(Address); public java.lang.String toString(); public Person(java.lang.String, Address); }
新的问题:如果定义一个Employee类继承Person,如何处理Employee构造函数里的name和address字段?假设没有新的参数,至少有两种选择:
// Option 1: define name and address as 'var' class Employee (var name: String, var address: Address) extends Person (name, address) { ... } // Option 2: define name and address without var or val class Employee (name: String, address: Address) extends Person (name, address) { ... }
因为Scala已经为Person类里的name和address声明了getter和setter方法,解决方法是不使用var进行声明:
// this is correct class Employee (name: String, address: Address) extends Person (name, address) { ... }
把下面代码编译反编译,文件名是Person.scala,反编译Employee.class
case class Address (city: String, state: String) class Person (var name: String, var address: Address) { override def toString = if (address == null) name else s"$name @ $address" } class Employee (name: String, address: Address) extends Person (name, address) { // code here ... }
反编译结果如下:
$ javap Employee Compiled from "Person.scala" public class Employee extends Person implements scala.ScalaObject{ public Employee(java.lang.String, Address); }
- Employee继承Person,Scala不为name和address字段生成getter和setter方法,Employee类继承了Person类的行为。
4.11 调用超类构造函数
- 问题:想要控制当创建子类构造函数时调用的父类构造函数
4.11.1 解决方案
- 这有一个问题,你可以控制子类的主构造函数调用的父类构造函数,但是不可以控制子类的辅助构造函数调用的父类构造函数。
下面例子,定义一个Dog类去调用Animal类的主构造函数:
class Animal (var name: String) { // ... } class Dog (name: String) extends Animal (name) { // ... }
如果Animal类有多个构造函数,那么Dog类的主构造函数可以调用任意一个
// (1) primary constructor class Animal (var name: String, var age: Int) { // (2) auxiliary constructor def this (name: String) { this(name, 0) } override def toString = s"$name is $age years old" } // calls the Animal one-arg constructor class Dog (name: String) extends Animal (name) { println("Dog constructor called") } // call the two-arg constructor class Dog (name: String) extends Animal (name, 0) { println("Dog constructor called") }
4.11.2 辅助构造函数
辅助构造函数的第一行必须调用当前类的另一个构造函数,不可能调用父类的构造函数
case class Address (city: String, state: String) case class Role (role: String) class Person (var name: String, var address: Address) { // no way for Employee auxiliary constructors to call this constructor def this (name: String) { this(name, null) address = null } override def toString = if (address == null) name else s"$name @ $address" } class Employee (name: String, role: Role, address: Address) extends Person (name, address) { def this (name: String) { this(name, null, null) } def this (name: String, role: Role) { this(name, role, null) } def this (name: String, address: Address) { this(name, null, address) } }
4.12 使用抽象类(Abstract Class)
- 问题:Scala有特质(trait),而且特质比抽象类更灵活,那么什么时候使用抽象类
4.12.1 解决方案
- 以下两点使用抽象类:
- 创建一个需要构造函数参数的基类
- Scala代码会被Java代码调用
特质不允许有构造函数参数:
// this won't compile trait Animal(name: String) // this compile abstract class Animal(name: String)
- 17.7章解决特质实现的方法不能被Java代码调用的问题
4.12.2 讨论
- 一个类智能继承一个抽象类。
声明抽象方法:
def speak // no body makes the method abstract
抽象方法不需要使用abstract关键词,去除方法的body就会变成抽象方法。这和在特质里定义抽象方法是一致的。
abstract class BaseController(db: Database) { def save { db.save } def update { db.update } def delete { db.delete } // abstract def connect // an abstract method that returns a String def getStatus: String // an abstract method that takes a parameter def setServerName(serverName: String) }
子类继承之后需要实现抽象方法或者继续声明成抽象的,不实现方法会报出“class needs to be abstract”错误
scala> class WidgetController(db: Database) extends BaseController(db) <console>:9: error: class WidgetController needs to be abstract, since: method setServerName in class BaseController of type (serverName: String)Unit is not defined method getStatus in class BaseController of type => String is not defined method connect in class BaseController of type => Unit is not defined class WidgetController(db: Database) extends BaseController(db) ^
- 因为类只能继承一个抽象类,当决定使用特质还是抽象类时一般使用特质,除非基类需要构造函数参数
4.13 在抽象基类(或特质)中定义属性
- 问题:在抽象基类(或特质)中定义抽象或具体属性可供所有子类引用
4.13.1 解决方案
- 可以在抽象类或者特质里声明val和var字段。这些字段可以是抽象或者有具体实现。
4.13.2 抽象的val和var字段
下面抽象类有抽象的val和var字段,一个简单的具体方法:
abstract class Pet (name: String) { val greeting: String var age: Int def sayHello { println(greeting) } override def toString = s"I say $greeting, and I'm $age" }
子类继承抽象类,然后为抽象的字段赋值,注意这些字段还是指定成val或者var:
class Dog (name: String) extends Pet (name) { val greeting = "Woof" var age = 2 } class Cat (name: String) extends Pet (name) { val greeting = "Meow" var age = 5 }
object中演示调用:
object AbstractFieldsDemo extends App { val dog = new Dog("Fido") val cat = new Cat("Morris") dog.sayHello cat.sayHello println(dog) println(cat) // verify that the age can be changed cat.age = 10 println(cat) }
结果输出:
Woof Meow I say Woof, and I'm 2 I say Meow, and I'm 5 I say Meow, and I'm 10
4.13.3 讨论
- 抽象类(或特质)里抽象字段的运行如下:
- 一个抽象的var字段会自动生成getter和setter方法
- 一个抽象的val字段会自动生成getter方法
- 当在抽象类或特质里定义一个抽象字段,Scala编译器不会在结果代码里创建一个字段,只会根据val或者var生成相应的方法
上面的代码通过 scalac -Xprint:all,或者反编译Pet.class文件,会发现没有greeting或者age字段。反编译输出如下:
import scala.*; import scala.runtime.BoxesRunTime; public abstract class Pet { public abstract String greeting(); public abstract int age(); public abstract void age_$eq(int i); public void sayHello() { Predef$.MODULE$.println(greeting()); } public String toString(){ // code omitted } public Pet(String name){} }
- 所以当你在具体的类里给这些字段提供具体的值时,必须重新定义字段为val或者var。因为在抽象类或者特质里这些字段实际并不存在,所以override关键词并不需要。
另一个结果,可以在抽象基类使用def定义无参取代使用val定义,然后可以在具体类里定义成val。
abstract class Pet (name: String) { def greeting: String } class Dog (name: String) extends Pet (name) { val greeting = "Woof" } object Test extends App { val dog = new Dog("Fido") println(dog.greeting) }
4.13.4 抽象类里具体的val字段
抽象类里定义一个具体的val字段可以提供一个初始化值,然后可以在具体子类重写那个值。
abstract class Animal { val greeting = "Hello" // provide an initial value def sayHello { println(greeting) } def run } class Dog extends Animal { override val greeting = "Woof" // override the value def run { println("Dog is running") } }
上面例子中,两个类中都创建了greeting字段
abstract class Animal { val greeting = { println("Animal"); "Hello" } } class Dog extends Animal { override val greeting = { println("Dog"); "Woof" } } object Test extends App { new Dog }
结果输出:
Animal Dog
可以反编译Animal和Dog类,greeting字段声明成如下:
private final String greeting = "Hello";
抽象类中字段声明成final val那么具体子类中就不能重写这个字段的值:
abstract class Animal { final val greeting = "Hello" // made the field 'final' } class Dog extends Animal { val greeting = "Woof" // this line won't compile }
4.13.5 抽象类里具体var字段
可以在抽象类或特质为var字段提供一个初始化值,然后在具体子类引用:
abstract class Animal { var greeting = "Hello" var age = 0 override def toString = s"I say $greeting, and I'm $age years old." } class Dog extends Animal { greeting = "Woof" //调用setter方法 age = 2 }
这些字段在抽象基类里声明并赋值,反编译Animal类如下:
private String greeting; private int age; public Animal(){ greeting = "Hello"; age = 0; } // more code ...
- 因为在Animal基类里这个字段已经声明并且初始化,所以在具体子类里没有必要重新声明字段。
Dog类使用 scalac -Xprint:all 编译:
class Dog extends Animal { def <init>(): Dog = { Dog.super.<init>(); Dog.this.greeting_=("Woof"); Dog.this.age_=(2); () } }
- 因为这个字段在抽象类里是具体的,他们只需要在具体子类里重新赋值即可
4.13.6 不要使用null
使用Option/Some/None模式初始化字段:
trait Animal { val greeting: Option[String] var age: Option[Int] = None override def toString = s"I say $greeting, and I'm $age years old." } class Dog extends Animal { val greeting = Some("Woof") age = Some(2) } object Test extends App { val d = new Dog println(d) }
输出如下:
I say Some(Woof), and I'm Some(2) years old.
4.14 Case类生成样本代码
- 问题: 在match表达式。actor或者其他使用case类生成样本代码的情况,生成包括获取器,修改器,apply,unapply,toString, equals和hashCode等等方法。
4.14.1 解决方案
定义一个case类如下:
// name and relation are 'val' by default case class Person(name: String, relation: String)
- 定义一个case类会生成很多样本代码,有以下好处:
- 生成apply方法,所以不需要使用new关键词去创建这个类的实例
- case类构造函数参数默认声明成val,会自动生成获取器方法,声明成var会自动生成获取器和修改器
- 生成默认的toString方法
- 生成unapply方法,可以在匹配表达式轻松使用case类
- 生成equals和hashCode方法
- 生成copy方法
定义case类,创建一个新的实例时不需要使用new关键词
scala> case class Person(name: String, relation: String) defined class Person // "new" not needed before Person scala> val emily = Person("Emily", "niece") emily: Person = Person(Emily,niece)
构造函数默认声明成val,所以会自动生成获取器方法,但不会生成修改器方法:
scala> emily.name res0: String = Emily scala> emily.name = "Fred" <console>:10: error: reassignment to val emily.name = "Fred" ^
构造函数参数声明成var,会自动生成获取器和修改器方法:
scala> case class Company (var name: String) defined class Company scala> val c = Company("Mat-Su Valley Programming") c: Company = Company(Mat-Su Valley Programming) scala> c.name res0: String = Mat-Su Valley Programming scala> c.name = "Valley Programming" c.name: String = Valley Programming
Case类有一个默认的toString方法实现:
scala> emily res0: Person = Person(Emily,niece)
自动生成提取器方法(unapply),当需要在匹配表达式提取信息时很好用(构造器从给定的参数列表创建一个对象, 而提取器却是从传递给它的对象中提取出构造该对象的参数):
scala> emily match { case Person(n, r) => println(n, r) } (Emily,niece)
自动生成equals和hashCode方法,实例可以如下方法比较:
scala> val hannah = Person("Hannah", "niece") hannah: Person = Person(Hannah,niece) scala> emily == hannah res1: Boolean = false
自动创建copy方法,当需要clone一个对象时很有帮助,在运行过程中还可以改变一些字段:
scala> case class Employee(name: String, loc: String, role: String) defined class Employee scala> val fred = Employee("Fred", "Anchorage", "Salesman") fred: Employee = Employee(Fred,Anchorage,Salesman) scala> val joe = fred.copy(name="Joe", role="Mechanic") joe: Employee = Employee(Joe,Anchorage,Mechanic)
4.14.2 讨论
- case类主要目的是创建“不可变的记录”,这样可以很容易的在模式匹配表达式里使用。
4.14.3 生成的代码
文件Person.scala:
case class Person(var name: String, var age: Int)
编译后会创建两个class文件,Person.class和Person$.class
$ scalac Person.scala
反编译Person.class
$ javap Person //结果 Compiled from "Person.scala" public class Person extends java.lang.Object ↵ implements scala.ScalaObject,scala.Product,scala.Serializable{ public static final scala.Function1 tupled(); public static final scala.Function1 curry(); public static final scala.Function1 curried(); public scala.collection.Iterator productIterator(); public scala.collection.Iterator productElements(); public java.lang.String name(); public void name_$eq(java.lang.String); public int age(); public void age_$eq(int); public Person copy(java.lang.String, int); public int copy$default$2(); public java.lang.String copy$default$1(); public int hashCode(); public java.lang.String toString(); public boolean equals(java.lang.Object); public java.lang.String productPrefix(); public int productArity(); public java.lang.Object productElement(int); public boolean canEqual(java.lang.Object); public Person(java.lang.String, int); }
反编译Person$.class
$ javap Person$ //结果 Compiled from "Person.scala" public final class Person$ extends scala.runtime.AbstractFunction2 ↵ implements scala.ScalaObject,scala.Serializable{ public static final Person$ MODULE$; public static {}; public final java.lang.String toString(); public scala.Option unapply(Person); public Person apply(java.lang.String, int); public java.lang.Object readResolve(); public java.lang.Object apply(java.lang.Object,java.lang.Object); }
去掉case,然后编译反编译如下:
public class Person extends java.lang.Object{ public java.lang.String name(); public void name_$eq(java.lang.String); public int age(); public void age_$eq(int); public Person(java.lang.String, int); }
如果不需要那么多额外的函数,考虑使用正常的类。如果只想创建一个不适用new关键词创建实例的类,如下使用:
val p = Person("Alex")
此时,可以创建一个apply方法。详细看6.8章
查看更多
4.15 定义一个equals方法(对象相等)
- 问题: 类中定义一个equals方法比较对象实例
4.15.1 解决方案
和Java一样,定义一个equals(和hashCode)方法比较两个实例,和Java不同的是,然后可以使用 == 方法比较两个实例是否相等。
class Person (name: String, age: Int) { def canEqual(a: Any) = a.isInstanceOf[Person] override def equals(that: Any): Boolean = that match { case that: Person => that.canEqual(this) && this.hashCode == that.hashCode case _ => false } override def hashCode:Int = { val prime = 31 var result = 1 result = prime * result + age; result = prime * result + (if (name == null) 0 else name.hashCode) return result } }
- 上面例子显示的是一个修改后的hashCode方法。
使用 == 方法比较两个实例:
import org.scalatest.FunSuite class PersonTests extends FunSuite { // these first two instances should be equal val nimoy = new Person("Leonard Nimoy", 82) val nimoy2 = new Person("Leonard Nimoy", 82) val shatner = new Person("William Shatner", 82) val ed = new Person("Ed Chigliak", 20) // all tests pass test("nimoy == nimoy") { assert(nimoy == nimoy) } test("nimoy == nimoy2") { assert(nimoy == nimoy2) } test("nimoy2 == nimoy") { assert(nimoy2 == nimoy) } test("nimoy != shatner") { assert(nimoy != shatner) } test("shatner != nimoy") { assert(shatner != nimoy) } test("nimoy != null") { assert(nimoy != null) } test("nimoy != String") { assert(nimoy != "Leonard Nimoy") } test("nimoy != ed") { assert(nimoy != ed) } }
- 上面的测试创建在ScalaTest FunSuite,和JUnit单元测试类似
4.15.2 讨论
- Java中 == 操作符比较引用相等,Scala中 == 是比较两个实例是否相等的方法。
当使用继承时依旧可以继续使用上面的方法
class Employee(name: String, age: Int, var role: String) extends Person(name, age) { override def canEqual(a: Any) = a.isInstanceOf[Employee] override def equals(that: Any): Boolean = that match { case that: Employee => that.canEqual(this) && this.hashCode == that.hashCode case _ => false } //上面case that: Employee保证that是Employee类型,that.canEqual(this)保证this也是Employee类型 override def hashCode:Int = { val ourHash = if (role == null) 0 else role.hashCode super.hashCode + ourHash } }
上面的代码使用canEqual,equals,hashCode相同方式,而且是一致的,尤其是比较子类实例和其父类实例
class EmployeeTests extends FunSuite with BeforeAndAfter { // these first two instance should be equal val eNimoy1 = new Employee("Leonard Nimoy", 82, "Actor") val eNimoy2 = new Employee("Leonard Nimoy", 82, "Actor") val pNimoy = new Person("Leonard Nimoy", 82) val eShatner = new Employee("William Shatner", 82, "Actor") test("eNimoy1 == eNimoy1") { assert(eNimoy1 == eNimoy1) } test("eNimoy1 == eNimoy2") { assert(eNimoy1 == eNimoy2) } test("eNimoy2 == eNimoy1") { assert(eNimoy2 == eNimoy1) } test("eNimoy != pNimoy") { assert(eNimoy1 != pNimoy) } test("pNimoy != eNimoy") { assert(pNimoy != eNimoy1) } }
4.15.3 理论
- Scaladoc表述:“这个方法的任何实现都应该是等价关系”,等价关系应该有以下3个特征:
- 自反性(reflexive):Any类型的实例x,x.equals(x)返回true
- 对称性(symmetric):Any类型的实例x和y,x.equals(y)和y.equals(x)返回true
- 传递性(transitive):AnyRef的实例x,y和z,如果x.equals(y)和y.equals(z)返回true,那么x.equals(z)也返回true
因此如果重写equals方法,应该确认你的实现保留了等价关系
查看更多
4.16 创建内部类
- 希望创建一个类作为内部类并且保持在公开API之外,或者否则封装你的代码
4.16.1 解决方案
在一个类里面声明另一个类
class PandorasBox { case class Thing (name: String) var things = new collection.mutable.ArrayBuffer[Thing]() things += Thing("Evil Thing #1") things += Thing("Evil Thing #2") def addThing(name: String) { things += new Thing(name) } }
PandorasBox类的使用者不需要担心Thing的实现就能获得things集合
object ClassInAClassExample extends App { val p = new PandorasBox p.addThing("Evil Thing #3") p.addThing("Evil Thing #4") p.things.foreach(println) }
4.16.2 讨论
Scala和Java不同,“不同于Java语言内部类是封闭类的成员,Scala中内部类和外部对象(object)绑定”:
object ClassInObject extends App { // inner classes are bound to the object val oc1 = new OuterClass val oc2 = new OuterClass val ic1 = new oc1.InnerClass val ic2 = new oc2.InnerClass ic1.x = 10 ic2.x = 20 println(s"ic1.x = ${ic1.x}") println(s"ic2.x = ${ic2.x}") } class OuterClass { class InnerClass { var x = 1 } }
因为内部类绑定到他们的对象实例,打印如下:
ic1.x = 10 ic2.x = 20
更多用法,对象里包括类,类里包括对象:
object InnerClassDemo2 extends App { // class inside object println(new OuterObject.InnerClass().x) // object inside class println(new OuterClass().InnerObject.y) } object OuterObject { class InnerClass { var x = 1 } } class OuterClass { object InnerObject { val y = 2 } }
查看更多